﻿// -----------------------------------------------------------------------
// Copyright (c) David Kean. All rights reserved.
// -----------------------------------------------------------------------
extern alias pcl;
using System;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using BackgroundWorker = pcl::System.ComponentModel.BackgroundWorker;
using RunWorkerCompletedEventArgs = pcl::System.ComponentModel.RunWorkerCompletedEventArgs;
using DoWorkEventArgs = pcl::System.ComponentModel.DoWorkEventArgs;
using AsyncOperation = pcl::System.ComponentModel.AsyncOperation;
using AsyncOperationManager = pcl::System.ComponentModel.AsyncOperationManager;

namespace Portable.ComponentModel
{
    [TestClass]
    public class BackgroundWorkerTests
    {
        [TestMethod]
        public void CancellationPending_DefaultsToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();

            Assert.IsFalse(worker.CancellationPending);
        }

        [TestMethod]
        public void IsBusy_DefaultsToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();

            Assert.IsFalse(worker.IsBusy);
        }
        
        [TestMethod]
        public void WorkerSupportsProgress_DefaultsToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();

            Assert.IsFalse(worker.WorkerReportsProgress);
        }

        [TestMethod]
        public void WorkerSupportsCancellation_DefaultsToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();

            Assert.IsFalse(worker.WorkerSupportsCancellation);
        }

        [TestMethod]
        public void ReportProgress1_WhenWorkerReportsProgressIsFalse_ThrowsInvalidOperation()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = false;

            ExceptionAssert.Throws<InvalidOperationException>(() =>
            {
                worker.ReportProgress(100);
            });
        }

        [TestMethod]
        public void ReportProgress2_WhenWorkerReportsProgressIsFalse_ThrowsInvalidOperation()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = false;

            ExceptionAssert.Throws<InvalidOperationException>(() =>
            {
                worker.ReportProgress(100, new object());
            });
        }

        [TestMethod]
        public void ReportProgress1_WhenIsBusyIsFalse_RaisesProgressChanged()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;
            int raiseCount = 0;
            worker.ProgressChanged += (sender, e) => raiseCount++;

            worker.ReportProgress(100);

            Assert.AreEqual(1, raiseCount);
        }

        [TestMethod]
        public void ReportProgress2_WhenIsBusyIsFalse_RaisesProgressChanged()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;
            int raiseCount = 0;
            worker.ProgressChanged += (sender, e) => raiseCount++;

            worker.ReportProgress(100, (object)null);

            Assert.AreEqual(1, raiseCount);
        }

        [TestMethod]
        public void ReportProgress1_WhenIsBusyIsFalse_PassesThisAsSenderToProgressChanged()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            object result = null;
            worker.ProgressChanged += (sender, e) => result = sender;
            worker.ReportProgress(100);

            Assert.AreSame(worker, result);
        }

        [TestMethod]
        public void ReportProgress2_WhenIsBusyIsFalse_PassesThisAsSenderToProgressChanged()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            object result = null;
            worker.ProgressChanged += (sender, e) => result = sender;
            worker.ReportProgress(100, (object)null);

            Assert.AreSame(worker, result);
        }

        [TestMethod]
        public void ReportProgress1_WhenIsBusyIsFalse_SetsProgressPercentageToPercentProgress()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            int? result = null;
            worker.ProgressChanged += (sender, e) => result = e.ProgressPercentage;
            worker.ReportProgress(76);

            Assert.AreEqual(76, result.Value);
        }

        [TestMethod]
        public void ReportProgress2_WhenIsBusyIsFalse_SetsProgressPercentageToPercentProgress()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            int? result = null;
            worker.ProgressChanged += (sender, e) => result = e.ProgressPercentage;
            worker.ReportProgress(76, (object)null);

            Assert.AreEqual(76, result.Value);
        }

        [TestMethod]
        public void ReportProgress1_WhenIsBusyIsFalse_SetsUserStateToNull()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            object result = null;
            worker.ProgressChanged += (sender, e) => result = e.UserState;
            worker.ReportProgress(76);

            Assert.IsNull(result);
        }

        [TestMethod]
        public void ReportProgress2_WhenIsBusyIsFalse_SetsUserStateToUserState()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerReportsProgress = true;

            object result = null;
            var userState = new object();
            worker.ProgressChanged += (sender, e) => result = e.UserState;
            worker.ReportProgress(76, userState);

            Assert.AreSame(result, userState);
        }

        [TestMethod]
        public void RunWorkerAsync1_RaisesDoWorkWithArgumentSetToNull()
        {
            DoWorkEventArgs result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { result = e; };
            worker.RunWorkerAsync();
            
            Assert.IsNull(result.Argument);
        }

        [TestMethod]
        public void RunWorkerAsync2_NullAsArgument_RaisesDoWorkWithArgumentSetToNull()
        {
            DoWorkEventArgs result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { result = e;};
            worker.RunWorkerAsync((object)null);

            Assert.IsNull(result.Argument);
        }

        [TestMethod]
        public void RunWorkerAsync2_ValueAsArgument_RaisesDoWorkWithArgumentSetToArgument()
        {
            DoWorkEventArgs result = null;

            object argument = new object();
            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { result = e; };
            worker.RunWorkerAsync(argument);
            
            Assert.AreSame(argument, result.Argument);
        }


        [TestMethod]
        public void RunWorkerAsync1_SetsCancellationPendingToFalse()
        {
            bool? result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerSupportsCancellation = true;
            worker.CancelAsync();
            worker.DoWork += (sender, e) => { result = worker.CancellationPending; };
            worker.RunWorkerAsync();

            Assert.IsFalse(result.Value);
        }

        [TestMethod]
        public void RunWorkerAsync2_SetsCancellationPendingToFalse()
        {
            bool? result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerSupportsCancellation = true;
            worker.CancelAsync();
            worker.DoWork += (sender, e) => { result = worker.CancellationPending; };
            worker.RunWorkerAsync(new object());

            Assert.IsFalse(result.Value);
        }

        [TestMethod]
        public void RunWorkerAsync1_WhileRunning_SetsIsBusyToTrue()
        {
            bool? result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { result = worker.IsBusy; };
            worker.RunWorkerAsync();

            Assert.IsTrue(result.Value);
        }

        [TestMethod]
        public void RunWorkerAsync2_WhileRunning_SetsIsBusyToTrue()
        {
            bool? result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { result = worker.IsBusy; };
            worker.RunWorkerAsync(new object());

            Assert.IsTrue(result.Value);
        }

        [TestMethod]
        public void RunWorkerAsync1_WhenFinished_SetsIsBusyToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.RunWorkerAsync();

            Assert.IsFalse(worker.IsBusy);
        }

        [TestMethod]
        public void RunWorkerAsync2_WhenFinished_SetsIsBusyToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.RunWorkerAsync(new object());

            Assert.IsFalse(worker.IsBusy);
        }

        [TestMethod]
        public void RunWorkerAsync1_WhenFinished_SetsCancellationPendingToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { worker.CancelAsync(); };
            worker.RunWorkerAsync();

            Assert.IsFalse(worker.CancellationPending);
        }

        [TestMethod]
        public void RunWorkerAsync2_WhenFinished_SetsCancellationPendingToFalse()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { worker.CancelAsync(); };
            worker.RunWorkerAsync(new object());

            Assert.IsFalse(worker.CancellationPending);
        }

        [TestMethod]
        public void RunWorkerAsync1_WhileRunning_ThrowsInvalidOperation()
        {
            Exception exception = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => 
            {
                try
                {
                    worker.RunWorkerAsync(new object());
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
            };

            worker.RunWorkerAsync();

            Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
        }

        [TestMethod]
        public void RunWorkerAsync2_WhileRunning_ThrowsInvalidOperation()
        {
            Exception exception = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) =>
            {
                try
                {
                    worker.RunWorkerAsync(new object());
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
            };

            worker.RunWorkerAsync(new object());

            Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
        }

        [TestMethod]
        public void RunWorkerAsync1_CallsSynchronizationContextCurrentOperationStarted()
        {
            int callCount = 0;
            var context = SynchronizationContextFactory.OverrideOperationStarted(() => callCount++);
            var worker = CreateSynchronousBackgroundWorker(context);
            worker.RunWorkerAsync();

            Assert.AreEqual(1, callCount);
        }

        [TestMethod]
        public void RunWorkerAsync2_CallsSynchronizationContextCurrentOperationStarted()
        {
            int callCount = 0;
            var context = SynchronizationContextFactory.OverrideOperationStarted(() => callCount++);
            var worker = CreateSynchronousBackgroundWorker(context);
            worker.RunWorkerAsync(new object());

            Assert.AreEqual(1, callCount);
        }

        [TestMethod]
        public void RunWorkerAsync1_CallsSynchronizationContextCurrentOperationCompleted()
        {
            int callCount = 0;
            var context = SynchronizationContextFactory.OverrideOperationCompleted(() => callCount++);

            var worker = CreateSynchronousBackgroundWorker(context);
            worker.RunWorkerAsync();

            Assert.AreEqual(1, callCount);
        }

        [TestMethod]
        public void RunWorkerAsync2_CallsSynchronizationContextCurrentOperationCompleted()
        {
            int callCount = 0;
            var context = SynchronizationContextFactory.OverrideOperationCompleted(() => callCount++);

            var worker = CreateSynchronousBackgroundWorker(context);
            worker.RunWorkerAsync(new object());

            Assert.AreEqual(1, callCount);
        }

        [TestMethod]
        public void DoWork_WhenCancelIsSetToTrue_RunWorkerCompletedIsRaisedWithCancelledSetToTrue()
        {
            RunWorkerCompletedEventArgs result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => e.Cancel = true;
            worker.RunWorkerCompleted += (sender, e) => result = e;
            worker.RunWorkerAsync();

            Assert.IsTrue(result.Cancelled);
        }

        [TestMethod]
        public void DoWork_WhenResultIsSetToValue_RunWorkerCompletedIsRaisedWithResultSetToValue()
        {
            object o = new object();
            RunWorkerCompletedEventArgs result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { e.Result = o; };
            worker.RunWorkerCompleted += (sender, e) => result = e;
            worker.RunWorkerAsync();

            Assert.AreSame(o, result.Result);
        }

        [TestMethod]
        public void DoWork_WhenFinished_RunWorkerCompletedIsRaisedWithUserStateToNull()
        {
            RunWorkerCompletedEventArgs result = null;

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { };
            worker.RunWorkerCompleted += (sender, e) => result = e;
            worker.RunWorkerAsync();

            Assert.IsNull(result.UserState);
        }

        [TestMethod]
        public void DoWork_WhenUnhandledException_RunWorkerCompletedIsRaisedWithErrorSetToException()
        {
            RunWorkerCompletedEventArgs result = null;

            Exception exception = new Exception();

            var worker = CreateSynchronousBackgroundWorker();
            worker.DoWork += (sender, e) => { throw exception; };
            worker.RunWorkerCompleted += (sender, e) => result = e;
            worker.RunWorkerAsync();

            Assert.AreSame(exception, result.Error);
        }

        [TestMethod]
        public void CancelAsync_WhenWorkSupportsCancellationIsFalse_ThrowsInvalidOperation()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerSupportsCancellation = false;

            ExceptionAssert.Throws<InvalidOperationException>(() =>
            {
                worker.CancelAsync();
            });
        }

        [TestMethod]
        public void CancelAsync_SetsCancellationPendingToTrue()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerSupportsCancellation = true;

            worker.CancelAsync();

            Assert.IsTrue(worker.CancellationPending);
        }

        [TestMethod]
        public void CancelAsync_CanBeCalledMultipleTimes()
        {
            var worker = CreateSynchronousBackgroundWorker();
            worker.WorkerSupportsCancellation = true;

            worker.CancelAsync();
            worker.CancelAsync();

            Assert.IsTrue(worker.CancellationPending);
        }

        private static BackgroundWorker CreateSynchronousBackgroundWorker(SynchronizationContext context)
        {
            Mock<PublicizedBackgroundWorker> worker = new Mock<PublicizedBackgroundWorker>();
            worker.CallBase = true;
            worker.Setup(w => w.CreateAsyncOperationPublic())
                  .Returns(AsyncOperationManager.CreateOperation(context, null));

            worker.Setup(w => w.RunOnBackgroundThreadPublic(It.IsAny<Action<object>>(), It.IsAny<object>()))
                  .Callback((Action<object> action, object argument) => action(argument));

            return worker.Object;
        }

        private static BackgroundWorker CreateSynchronousBackgroundWorker()
        {   // Returns a synchronous background worker to make unit testing easier
            // The integration tests will make sure that the asynchronous version works

            var context = SynchronizationContextFactory.OverridePost((d, state) => d(state));

            return CreateSynchronousBackgroundWorker(context);
        }

        public abstract class PublicizedBackgroundWorker : BackgroundWorker
        {
            internal override void QueueOnBackgroundThread(Action<object> action, object argument)
            {
                RunOnBackgroundThreadPublic(action, argument);
            }

            protected override AsyncOperation CreateAsyncOperation()
            {
                return CreateAsyncOperationPublic();
            }

            public abstract void RunOnBackgroundThreadPublic(Action<object> action, object argument);

            public abstract AsyncOperation CreateAsyncOperationPublic();

        }
    }
}
